using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Keyki.XNAGameEngine.Common.Console
{
	/// <summary>
	/// This is a game component that implements IUpdateable.
	/// </summary>
	public class XnaConsole : DrawableGameComponent
	{
		public XnaConsole(Game game, SpriteFont font)
			: base(game)
		{
			m_device = game.GraphicsDevice;
			m_spriteBatch = new SpriteBatch(m_device);
			m_font = font;
			m_background = new Texture2D(m_device, 1, 1, true, SurfaceFormat.Color);
			m_background.SetData(new[] {new Color(0, 0, 0, 125)});

			m_outputBuffer = "";

			m_history = new History(LinesDisplayed - 1); //last one for input field
			m_keyTimes = new Dictionary<Keys, double>();

			WriteLine("###");
			WriteLine("### " + Version);
			WriteLine("###");

			m_consoleXSize = Game.Window.ClientBounds.Right - Game.Window.ClientBounds.Left;
			m_consoleYSize = font.LineSpacing*LinesDisplayed;

			m_charXSize = (int)font.MeasureString("a").X;
			m_cursorPos = new Vector2(m_charXSize, m_consoleYSize - m_font.LineSpacing);

			//calculate number of letters that fit on a line, using "a" as example character
			m_firstInterval = 500f;
			m_repeatInterval = 200f;

			m_state = ConsoleState.Closed;
			m_currentKeyState = Keyboard.GetState();
			m_lastKeyState = m_currentKeyState = Keyboard.GetState();

			for (int i = 0; i < Enum.GetValues(typeof (Keys)).Length; i++)
			{
				var key = (Keys) Enum.GetValues(typeof (Keys)).GetValue(i);
				m_keyTimes[key] = m_firstInterval;
			}
		}

		/// <summary>
		/// Allows the game component to update itself.
		/// </summary>
		/// <param name="gameTime">Provides a snapshot of timing values.</param>
		public override void Update(GameTime gameTime)
		{
			m_lastKeyState = m_currentKeyState;
			m_currentKeyState = Keyboard.GetState();
			double elapsedTime = gameTime.ElapsedGameTime.TotalMilliseconds; //time since last update call

			//get console dimensions
			m_consoleXSize = Game.Window.ClientBounds.Right - Game.Window.ClientBounds.Left;
			m_consoleYSize = m_font.LineSpacing*LinesDisplayed;

			//if Tilda pressed change console state
			if (IsKeyPressed(Keys.OemTilde))
			{
				m_state = m_state == ConsoleState.Open ? ConsoleState.Closed : ConsoleState.Open;
				return;
			}

			//don't draw the console if it's closed
			Visible = m_state != ConsoleState.Closed;

			//doing nothing if console closed
			if (m_state != ConsoleState.Open)
				return;

			string nextChar = GetStringFromKeyState(elapsedTime);

			if (!String.IsNullOrEmpty(nextChar))
			{
				int position = (int)m_cursorPos.X/m_charXSize;
				if (position > m_outputBuffer.Length)
					m_outputBuffer += nextChar;
				else
					m_outputBuffer = m_outputBuffer.Insert(position, nextChar);

				m_cursorPos.X += nextChar.Length * m_charXSize;
				m_cursorPos.Y = m_consoleYSize - m_font.LineSpacing;
			}

			//if enter pressed add string to history
			if (IsKeyPressed(Keys.Enter))
			{
				m_history.Add(m_outputBuffer);
				m_outputBuffer = "";
				m_cursorPos.X = m_charXSize;
			}
			else if (KeyPressWithRepeat(Keys.Back, elapsedTime))
			{
				int position = (int) m_cursorPos.X/m_charXSize - 1;

				if (position > 0)
				{
					string substring = m_outputBuffer.Substring(position);

					m_outputBuffer = m_outputBuffer.Remove(position - 1);
					m_outputBuffer += substring;
					m_cursorPos.X -= m_charXSize;
				}
			}
			else if (KeyPressWithRepeat(Keys.Delete, elapsedTime))
			{
				int position = (int)m_cursorPos.X / m_charXSize - 1;

				if (position < m_outputBuffer.Length)
				{
					string substring = m_outputBuffer.Substring(position + 1);

					m_outputBuffer = m_outputBuffer.Remove(position);
					m_outputBuffer += substring;
				}
			}

			else if (KeyPressWithRepeat(Keys.PageUp, elapsedTime))
			{
				m_history.Previous();
			}
			else if (KeyPressWithRepeat(Keys.PageDown, elapsedTime))
			{
				m_history.Next();
			}

			//Cursor moving
			if (KeyPressWithRepeat(Keys.Up, elapsedTime))
			{
				m_outputBuffer = m_history.Previous();
				m_cursorPos.X = (m_outputBuffer.Length + 1) * m_charXSize;
			}
			else if (KeyPressWithRepeat(Keys.Down, elapsedTime))
			{
				m_outputBuffer = m_history.Next();
				m_cursorPos.X = (m_outputBuffer.Length + 1) * m_charXSize;
			}
			else if (KeyPressWithRepeat(Keys.Left, elapsedTime))
			{
				m_cursorPos.X = m_cursorPos.X > m_charXSize ? m_cursorPos.X - m_charXSize : m_charXSize;
			}
			else if (KeyPressWithRepeat(Keys.Right, elapsedTime))
			{
				m_cursorPos.X = m_cursorPos.X < m_consoleXSize - m_charXSize ? m_cursorPos.X + m_charXSize : m_consoleXSize - m_charXSize;
			}
			else if (IsKeyPressed(Keys.Home))
			{
				m_cursorPos.X = m_charXSize;
			}
			else if (IsKeyPressed(Keys.End))
			{
				m_cursorPos.X = m_outputBuffer.Length*m_charXSize;
			}

			base.Update(gameTime);
		}

		public override void Draw(GameTime gameTime)
		{
			var posVector = new Vector2(0, 0);

			m_spriteBatch.Begin();

			m_spriteBatch.Draw(m_background, new Rectangle(0, 0, m_consoleXSize, m_consoleYSize), Color.White);

			foreach (string line in m_history)
			{
				m_spriteBatch.DrawString(m_font, line, posVector, Color.White);
				posVector.Y += m_font.LineSpacing;
			}

			posVector.Y = m_consoleYSize - m_font.LineSpacing;
			m_spriteBatch.DrawString(m_font, ">", posVector, Color.White);
			posVector.X = m_charXSize;
			m_spriteBatch.DrawString(m_font, m_outputBuffer, posVector, Color.White);

			m_spriteBatch.DrawString(m_font, "_", m_cursorPos, Color.White);

			m_spriteBatch.End();

			base.Draw(gameTime);
		}

		#region public Methods

		/// <summary>
		/// Write to the console
		/// </summary>
		/// <param name="str">string to write</param>
		public void Write(string str)
		{
			m_outputBuffer += str;
		}

		/// <summary>
		/// Write a line to the console
		/// </summary>
		/// <param name="str">line to write</param>
		public void WriteLine(string str)
		{
			m_history.Add(str);
		}

		#endregion

		#region private Methods

		//check if the key has just been pressed
		private bool IsKeyPressed(Keys key)
		{
			return m_currentKeyState.IsKeyDown(key) && !m_lastKeyState.IsKeyDown(key);
		}

		//check if a key is pressed, and repeat it at the default repeat rate
		private bool KeyPressWithRepeat(Keys key, double elapsedTime)
		{
			if (m_currentKeyState.IsKeyDown(key))
			{
				if (IsKeyPressed(key)) return true; //if the key has just been pressed, it automatically counts
				m_keyTimes[key] -= elapsedTime; //count down to next repeat
				if (m_keyTimes[key] < 0) //if the time has run out, repeat the letter
				{
					m_keyTimes[key] = m_repeatInterval; //reset the timer to the repeat interval
					return true;
				}
				return false;
			}
			//if the key is not pressed, reset it's time to the first interval, which is usually longer
			m_keyTimes[key] = m_firstInterval;
			return false;
		}

		/// <summary>
		/// Takes keyboard input and returns certain characters as a string
		/// </summary>
		/// <param name="elapsedTime"></param>
		/// <returns></returns>
		private string GetStringFromKeyState(double elapsedTime)
		{
			bool shiftPressed = m_currentKeyState.IsKeyDown(Keys.LeftShift) || m_currentKeyState.IsKeyDown(Keys.RightShift);
			//bool altPressed = m_currentKeyState.IsKeyDown(Keys.LeftAlt) || m_currentKeyState.IsKeyDown(Keys.RightAlt);

			Keys[] pressedKeys = m_currentKeyState.GetPressedKeys();

			foreach (Keys key in pressedKeys)
			{
				if (KeyPressWithRepeat(key, elapsedTime))
				{
					KeyBinding keyBinding;
					KeyboardHelper.AmericanBindings.TryGetValue(key, out keyBinding);
					return shiftPressed ? keyBinding.ShiftString : keyBinding.UnmodifiedString;
				}
			}

			return "";
		}

		#endregion

		#region Nested type: ConsoleState

		private enum ConsoleState
		{
			Closed,
			Open
		}

		#endregion

		#region Configuration constants

		private const int LinesDisplayed = 14;
		private const string Version = "Xna Console v.1.0";

		#endregion

		#region Rendering stuff

		private readonly Texture2D m_background;
		private readonly GraphicsDevice m_device;
		private readonly SpriteFont m_font;
		private readonly SpriteBatch m_spriteBatch;

		#endregion

		#region Console text management stuff

		private readonly double m_firstInterval;
		private readonly History m_history;
		private readonly Dictionary<Keys, double> m_keyTimes;
		private readonly double m_repeatInterval;
		private int m_consoleXSize, m_consoleYSize;
		private int m_charXSize;
		private Vector2 m_cursorPos;
		private string m_outputBuffer;

		#endregion

		#region State and timing management stuff

		private KeyboardState m_currentKeyState;
		private KeyboardState m_lastKeyState;
		private ConsoleState m_state;
		private double m_stateStartTime;

		#endregion
	}
}